package lib.rpc;

import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;

import lib.ArgumentValidator;
import lib.logging.ILogger;
import lib.messaging.NetworkAddress;
import lib.paxos.AcceptMessage;
import lib.paxos.AckMessage;
import lib.paxos.DecideMessage;
import lib.paxos.PaxosException;
import lib.paxos.PrepareMessage;
import lib.paxos.SyncMessage;

/**
 * RPC methods invoke functionality on client side
 * @implements {@link IRPCMethods}
 * @author abhip
 *
 */
public class ClientRPCMethods implements IRPCMethods
{
    private IClientRPCFunctionality clientRPCFunctionality;
    private ILogger logger;

    public ClientRPCMethods(
            ILogger logger,
            IClientRPCFunctionality clientRPCFunctionality)
    {
        ArgumentValidator.IsNotNull("logger", logger);
        ArgumentValidator.IsNotNull("clientRPCFunctionality", clientRPCFunctionality);

        this.logger = logger;
        this.clientRPCFunctionality = clientRPCFunctionality;
    }

    @Override
    public AckMessage Accept(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            AcceptMessage acceptMsg)
                    throws ClassNotFoundException, IOException, PaxosException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsNotNull("acceptMsg", acceptMsg);

        if (clientAddress.equals(serverAddress))
        {
            throw new PaxosException(
                    String.format(
                            "ClientAddress and serverAddress are same for Accept RPC. " +
                                "This is not supported. ClientAddress=ServerAddress:%s",
                            clientAddress.toString()));
        }

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.AcceptMethodName,
                new Serializable[] { acceptMsg });

        RPCResultPacket rpcResultPacket;
        rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.AcceptMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

        return (AckMessage) rpcResultPacket.Result();
    }

    /**
     * Client side implementation of AppendLineToFile RPC
     * @param clientAddress
     * @param serverAddress
     * @param fileName
     * @param data
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public void AppendLineToFile(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            int roundID,
            String fileName,
            String data) throws ClassNotFoundException, IOException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsGreaterThan("roundID", roundID, 0);
        ArgumentValidator.IsNotNullOrEmpty("fileName", fileName);
        // data can be empty string, hence doing only null check
        ArgumentValidator.IsNotNull("data", data);

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.AppendLineToFileMethodName,
                new Serializable[] { roundID, fileName, data });

        RPCResultPacket rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.AppendLineToFileMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());
    }

    /**
     * Client side implementation of CreateFile RPC
     * @param clientAddress
     * @param serverAddress
     * @param fileName
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public void CreateFile(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            int roundID,
            String fileName) throws IOException, ClassNotFoundException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsGreaterThan("roundID", roundID, 0);
        ArgumentValidator.IsNotNullOrEmpty("fileName", fileName);

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.CreateFileMethodName,
                new Serializable[] { roundID, fileName });

        RPCResultPacket rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.CreateFileMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());
    }

    @Override
    public AckMessage Decide(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            DecideMessage decideMsg)
                    throws ClassNotFoundException, IOException, PaxosException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsNotNull("decideMsg", decideMsg);

        if (clientAddress.equals(serverAddress))
        {
            throw new PaxosException(
                    String.format(
                            "ClientAddress and serverAddress are same for Decide RPC. " +
                                "This is not supported. ClientAddress=ServerAddress:%s",
                            clientAddress.toString()));
        }

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.DecideMethodName,
                new Serializable[] { decideMsg });

        RPCResultPacket rpcResultPacket;
        rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.DecideMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

        return (AckMessage) rpcResultPacket.Result();
    }

    /**
     * Client side implementation of GetLineFromFile RPC
     * @param clientAddress
     * @param serverAddress
     * @param fileName
     * @param lineNumber
     * @return line read
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public String GetLineFromFile(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            int roundID,
            String fileName,
            int lineNumber) throws ClassNotFoundException, IOException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsGreaterThan("roundID", roundID, 0);
        ArgumentValidator.IsNotNullOrEmpty("fileName", fileName);
        ArgumentValidator.IsGreaterThan("lineNumber", lineNumber, 0);

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.GetLineFromFileMethodName,
                new Serializable[] { roundID, fileName, lineNumber });

        RPCResultPacket rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.AppendLineToFileMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

        return (rpcResultPacket.Result() == null)? null : rpcResultPacket.Result().toString();
    }

    private void HandleRPCFailure(
            RPCInvokePacket rpcInvokePacket,
            RPCResultPacket rpcResultPacket)
                    throws IOException, ClassNotFoundException
    {
        Object result = rpcResultPacket.Result();
        if (result != null)
        {
            if (result instanceof ClassNotFoundException)
            {
                ClassNotFoundException e = (ClassNotFoundException) result;
                throw e;
            }
            else if (result instanceof IOException)
            {
                IOException e = (IOException) result;
                throw e;
            }
        }

        logger.LogError(
                "RPC for %s failed! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.AppendLineToFileMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

    }

    @SuppressWarnings("unchecked")
    @Override
    public List<NetworkAddress> Join(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            int roundID,
            NetworkAddress newServerAddress) throws ClassNotFoundException, IOException 
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsGreaterThan("roundID", roundID, 0);
        ArgumentValidator.IsNotNull("newServerAddress", newServerAddress);

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.JoinMethodName,
                new Serializable[] { roundID, newServerAddress });

        RPCResultPacket rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.JoinMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());
        
        return (rpcResultPacket.Result() == null)? null : (List<NetworkAddress>) rpcResultPacket.Result();
    }

    @Override
    public AckMessage Prepare(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            PrepareMessage prepareMsg)
                    throws ClassNotFoundException, IOException, PaxosException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsNotNull("prepareMsg", prepareMsg);

        if (clientAddress.equals(serverAddress))
        {
            throw new PaxosException(
                    String.format(
                            "ClientAddress and serverAddress are same for Prepare RPC. " +
                                "This is not supported. ClientAddress=ServerAddress:%s",
                            clientAddress.toString()));
        }

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.PrepareMethodName,
                new Serializable[] { prepareMsg });

        RPCResultPacket rpcResultPacket;
        rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.PrepareMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

        return (AckMessage) rpcResultPacket.Result();
    }

    @Override
    public void Reconfigure(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            int roundID,
            List<NetworkAddress> newServerAddresses) 
                    throws ClassNotFoundException, IOException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsGreaterThan("roundID", roundID, 0);
        ArgumentValidator.IsNotNull("newServerAddresses", newServerAddresses);
        
        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.ReconfigureMethodName,
                new Serializable[] { roundID, (Serializable) newServerAddresses });

        RPCResultPacket rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.ReconfigureMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());
    }

    @Override
    public AckMessage Sync(
            NetworkAddress clientAddress,
            NetworkAddress serverAddress,
            SyncMessage syncMessage)
                    throws ClassNotFoundException, IOException, PaxosException
    {
        ArgumentValidator.IsNotNull("clientAddress", clientAddress);
        ArgumentValidator.IsNotNull("serverAddress", serverAddress);
        ArgumentValidator.IsNotNull("syncMessage", syncMessage);

        if (clientAddress.equals(serverAddress))
        {
            throw new PaxosException(
                    String.format(
                            "ClientAddress and serverAddress are same for Decide RPC. " +
                                "This is not supported. ClientAddress=ServerAddress:%s",
                            clientAddress.toString()));
        }

        RPCInvokePacket rpcInvokePacket = new RPCInvokePacket(
                clientAddress,
                serverAddress,
                UUID.randomUUID(),
                IRPCMethods.SyncMethodName,
                new Serializable[] { syncMessage });

        RPCResultPacket rpcResultPacket;
        rpcResultPacket = clientRPCFunctionality.Invoke(rpcInvokePacket);

        if (rpcResultPacket.ResultCode() != RPCResultCode.SUCCESS)
        {
            HandleRPCFailure(rpcInvokePacket, rpcResultPacket);
            return null;
        }

        logger.LogInfo(
                "RPC for %s was successful! rpcInvokePacket: %s, rpcResultPacket: %s",
                IRPCMethods.SyncMethodName,
                rpcInvokePacket.toString(),
                rpcResultPacket.toString());

        return (AckMessage) rpcResultPacket.Result();
    }
}
